/******************************************************************************
 * SVN­Version­Information:
 * ­­­­­­­­­­­­­­­­­­­­­­­­---------------------
 * $LastChangedRevision: 6 $
 * $LastChangedBy: c.a.duevel $
 * $LastChangedDate: 2009-08-26 15:12:37 +0000 (Wed, 26 Aug 2009) $
 * $HeadURL: http://robofight.googlecode.com/svn/trunk/Server/src/de/hft_stuttgart/botwar/server/DataBase.java $
 *******************************************************************************/
package de.hft_stuttgart.botwar.server;

import de.hft_stuttgart.botwar.common.commands.GameOverCmd;
import de.hft_stuttgart.botwar.common.models.NewRobotInfo;
import de.hft_stuttgart.botwar.common.models.GameResultInfo;
import de.hft_stuttgart.botwar.common.models.Password;
import de.hft_stuttgart.botwar.common.models.RobotInfo;
import de.hft_stuttgart.botwar.server.model.Armament;
import de.hft_stuttgart.botwar.server.model.Chassis;
import de.hft_stuttgart.botwar.database.Player;
import de.hft_stuttgart.botwar.server.model.RobotConfiguration;
import de.hft_stuttgart.botwar.database.Statistic;
import de.hft_stuttgart.botwar.exceptions.RegistrationUnsuccessfulException;
import de.hft_stuttgart.botwar.server.model.Ranking;
import de.hft_stuttgart.botwar.server.model.Shield;
import de.hft_stuttgart.botwar.server.remote.Login;
import de.hft_stuttgart.botwar.server.remote.RemotePlayer;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.security.auth.login.LoginException;
import org.apache.log4j.Logger;

/**
 * @author pmv-mail@gmx.de, c.a.duevel@gmail.com
 */
public class DataBase {

    private final String SEPERATOR = System.getProperty("file.separator");
    private final File DB_DIR = new File("." + SEPERATOR +"DB");
    private static Logger logger = Logger.getLogger(DataBase.class);
    private static DataBase singelton = null;
    private EntityManagerFactory emf;
    private static boolean dropTable = false;
    private static final String CHASSIS_FILE = "chassis.csv";
    private static final String SHIELD_FILE = "shield.csv";
    private static final String ARMAMENT_FILE = "armament.csv";


    private DataBase() {
        if (DB_DIR.exists()) {
            this.open();
        } else {
            this.create();
        }
    }

    //threadsave method, called only if it is needed.
    private static synchronized DataBase getSingeltonThreadsafe() {
        if (DataBase.singelton == null) {
            DataBase.singelton = new DataBase();
        }
        return DataBase.singelton;
    }

    /**
     * Returns the singelton instance from the DataBase class.
     * @return singelton instance
     */
    public static DataBase get() {
        if (DataBase.singelton == null) {
            return getSingeltonThreadsafe();
        }
        return DataBase.singelton;
    }

    /**
     * Call this before using the database to drop the tables.
     * @param drop true to delete the tables, otherwise false (default)
     */
    public static void dropTable(final boolean drop) {
        if (singelton != null) {
            throw new IllegalStateException(
                    "Database can't be droped if it is already used!");
        }
        dropTable = drop;
    }

    /**
     * opening the DB
     */
    private void open() {
        logger.info("-------------------------");
        logger.info("opening Database\n\t\"" + DB_DIR.getAbsolutePath() + "\"");
        if ("\\".equals(SEPERATOR)) {
            if (DataBase.dropTable) {
                emf = Persistence.createEntityManagerFactory("OpenDB_BS_Drop");
                // at first the robotparts needed!
                createTestRobotElements();
                createTestPlayer();
            } else {
                emf = Persistence.createEntityManagerFactory("OpenDB_BS");
            }
        } else if ("/".equals(SEPERATOR)) {
            if (DataBase.dropTable) {
                emf = Persistence.createEntityManagerFactory("OpenDB_S_Drop");
                // at first the robotparts needed!
                createTestRobotElements();
                createTestPlayer();
            } else {
                emf = Persistence.createEntityManagerFactory("OpenDB_S");
            }
        }
        logger.info("Database ready to use");
        logger.info("-------------------------");
    }

    /**
     * creating the DB and the testdata
     */
    private void create() {
        logger.info("creating Database\n\t\"" + DB_DIR.getAbsolutePath() + "\"");
        if ("\\".equals(SEPERATOR)) {
            emf = Persistence.createEntityManagerFactory("CreateDB_BS");
        } else if ("/".equals(SEPERATOR)) {
            emf = Persistence.createEntityManagerFactory("CreateDB_S");
        }
        // at first the robotparts needed!
        createTestRobotElements();
        createTestPlayer();

        logger.info("Complied. Database\n\t\"" + DB_DIR.getAbsolutePath() +
                "\"\n\tis created and ready to use.");
    }

    // create a testuser
    private void createTestPlayer() {
        logger.debug("create testplayer \"admin\"");
        try {
            register("admin",
                    new Password("admin").toString(),
                    "admin@admin.de");
            logger.info("testplayer \"admin\" created");
        } catch (NoSuchAlgorithmException ex) {
            logger.error("could not create testplayer \"admin\"", ex);
        } catch (IllegalArgumentException ex) {
            logger.error("could not create testplayer \"admin\"", ex);
        } catch (RegistrationUnsuccessfulException ex) {
            logger.error("could not create testplayer \"admin\"", ex);
        }

        logger.info("create testplayer \"admin2\"");
        try {
            register("admin2",
                    new Password("admin2").toString(),
                    "admin2@admin2.de");
            logger.info("testplayer \"admin2\" created");
        } catch (NoSuchAlgorithmException ex) {
            logger.error("could not create testplayer \"admin2\"", ex);
        } catch (IllegalArgumentException ex) {
            logger.error("could not create testplayer \"admin2\"", ex);
        } catch (RegistrationUnsuccessfulException ex) {
            logger.error("could not create testplayer \"admin2\"", ex);
        }
    }

    private void createStandardRobots(final String playerName) {
        logger.debug("create 2 standard robots for player " + playerName);
        NewRobotInfo newRobotInfo = new NewRobotInfo("Standard Robot",
                "Lynx", "Fireshield", "Lightning");
        insertRobot(newRobotInfo, playerName);
        insertRobot(newRobotInfo, playerName);
        logger.info("2 standard robots for player " + playerName + " created");
    }

    private void createTestRobotElements() {
        logger.debug("create test robot elements");
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        for (Armament arm : readArmamentList()) {
            em.persist(arm);
        }
        for (Shield shield : readShieldList()) {
            em.persist(shield);
        }
        for (Chassis chassis : readChassisList()) {
            em.persist(chassis);
        }
        em.getTransaction().commit();
        em.close();
        logger.info("test robot elements created");
    }

    /**
     *
     * @param user name/ id of the user
     * @param pwd password from the user
     * @return returns the new remoteplayer object for the user
     * @throws javax.security.auth.login.LoginException
     * thrown if the login fails
     */
    public RemotePlayer login(final String user, final String pwd)
            throws LoginException {
        EntityManager em = emf.createEntityManager();
        Player player = em.find(Player.class, user);

        if (player == null || !player.getPwd().equals(pwd)) {
            em.close();
            throw new LoginException("User not found!");
        }
        if (Login.isLoggedIn(user)) {
            throw new LoginException("You're already logged in!");
        }

        player.login();
        em.close();

        return new RemotePlayer(player);
    }

    /**
     * Updates the statistic: online time and last online time
     * @param player the player which will be logged out
     */
    public void logout(final Player player) {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Statistic stat = em.find(Statistic.class, player.getUsername());
        if (stat == null) {
            //don't throw something, until it is catched in the
            //kick-player task
            return;
        }
        player.logout(stat);
        em.merge(player.getStat());
        em.getTransaction().commit();
        em.close();
    }

    /**
     * @param user new name for the user
     * @param pwd new password for the user
     * @param email new e-mail address for the user
     * @throws
     * de.hft_stuttgart.botwar.exceptions.RegistrationUnsuccessfulException
     */
    public void register(final String user,
            final String pwd,
            final String email)
            throws RegistrationUnsuccessfulException {
        EntityManager em = emf.createEntityManager();
        //find the username
        Player player = em.find(Player.class, user);
        if (player != null) {
            em.close();
            throw new RegistrationUnsuccessfulException("username in use");
        }
        player = null;

        //find the e-mail
        Query q = em.createQuery(
                "select p from Player p where p.email = :email");
        q.setParameter("email", email);
        try {
            player = (Player) q.getSingleResult();
        } catch (NoResultException ex) {
            player = null;
        }
        if (player != null) {
            em.close();
            throw new RegistrationUnsuccessfulException("e-mail in use");
        }

        //create new player
        em.getTransaction().begin();
        em.persist(new Player(user, email, pwd));
        em.getTransaction().commit();
        em.close();

        //create standardbots
        createStandardRobots(user);

        //update the ranking
        Ranking.add(user);

    }

    /**
     * Returns the Statistic Object from the given user
     * @param username name of the player for which the statistic is wanted
     * @return statistic object from the user
     * @throws java.lang.IllegalArgumentException
     */
    public Statistic getStatistic(final String username)
            throws IllegalArgumentException {
        EntityManager em = emf.createEntityManager();

        Statistic stat = (Statistic) em.find(Statistic.class, username);
        if (stat == null) {
            em.close();
            throw new IllegalArgumentException("user not found!");
        }
        em.close();

        return stat;
    }

    /**
     * updates the Statistic with the result of a game
     * @param gameOverCmd the gameOverCmd which will be read to update
     * the statistic
     */
    public void updateStat(final GameOverCmd gameOverCmd) {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();

        for (GameResultInfo playerInfo : gameOverCmd.getPlayerList()) {
            Statistic playerStat = em.find(Statistic.class,
                    playerInfo.getPlayerId());
            if (playerStat != null) {
                playerStat.addExperience(playerInfo.getExperience());
                switch (playerInfo.getResult()) {
                    case WON:
                        playerStat.addWonMatches();
                        break;
                    case LOST:
                        playerStat.addLostMatches();
                        break;
                    case DRAW:
                        playerStat.addDrawMatches();
                        break;
                }
                em.merge(playerStat);
                Ranking.get(playerInfo.getPlayerId()).addCoins(
                        playerInfo.getExperience());
            }
        }

        em.getTransaction().commit();
        em.close();
    }

    /**
     * Called for every player who has played in a game, that is now finished.
     * @param player the db object "player" from the RemotePlayer object
     */
    public void gameFinished(final Player player) {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Statistic stat = em.find(Statistic.class, player.getUsername());
        if (stat == null) {
            em.close();
            throw new IllegalArgumentException("user not found!");
        }
        player.finishPlaying(stat);
        em.merge(player.getStat());
        em.getTransaction().commit();
        em.close();
    }

    /**
     * Returns all armaments which have a lower or the same level
     * @param level the level of a player
     * @return list of armaments
     */
    public List<Armament> getAvailableArmaments(final int level) {
        EntityManager em = emf.createEntityManager();

        Query q = em.createQuery(
                "select a from Armament a WHERE a.levelRank <= :level");
        q.setParameter("level", level);
        List<Armament> list = q.getResultList();
        em.close();
        return list;
    }

    /**
     * Returns all shields which have a lower or the same level
     * @param level level of a player
     * @return list of shields
     */
    public List<Shield> getAvailableShields(final int level) {
        EntityManager em = emf.createEntityManager();

        Query q = em.createQuery(
                "select s from Shield s WHERE s.levelRank <= :level");
        q.setParameter("level", level);
        List<Shield> list = q.getResultList();
        em.close();
        return list;
    }

    /**
     * Returns all chassis which have a lower or the same level
     * @param level level of a player
     * @return list of chassis
     */
    public List<Chassis> getAvailableChassises(final int level) {
        EntityManager em = emf.createEntityManager();

        Query q = em.createQuery(
                "select c from Chassis c WHERE c.levelRank <= :level");
        q.setParameter("level", level);
        List<Chassis> list = q.getResultList();
        em.close();
        return list;
    }

    /**
     * Returns all created robots from one player equal or
     * lower then the maxLevel
     * @param user the name from the player
     * @param maxLevel the highest wanted level
     * @return a list of robots
     */
    public List<RobotInfo> getAvailableRobots(final String user,
            final int maxLevel) {
        EntityManager em = emf.createEntityManager();

        Player player = em.find(Player.class, user);
        if (player == null) {
            em.close();
            throw new IllegalArgumentException("user " + user + "not found");
        }

        Collection<RobotConfiguration> list = player.getRobots();
        List<RobotInfo> resultList = new ArrayList(list.size());
        int i = 0;
        for (RobotConfiguration robot : list) {
            if (robot.getLevelRank() <= maxLevel) {
                resultList.add(robot.getInfo());
                i++;
            }
        }


        em.close();
        return resultList;
    }

    /**
     * @param newRobotInfo roboter to be insered
     * @throws java.lang.IllegalArgumentException
     * if the owner or a part is unknown
     */
    public void insertRobot(final NewRobotInfo newRobotInfo,
            final String playerName)
            throws IllegalArgumentException {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Player player = em.find(Player.class, playerName);
        Chassis chassis = em.find(Chassis.class, newRobotInfo.getChassisName());
        Armament armament = em.find(Armament.class,
                newRobotInfo.getArmamentName());
        Shield shield = em.find(Shield.class, newRobotInfo.getShieldName());

        if (shield == null || chassis == null || armament == null) {
            em.close();
            throw new IllegalArgumentException(
                    "One of the components are unknown.");
        }

        RobotConfiguration robot = new RobotConfiguration(
                newRobotInfo.getRobotName(), player, chassis, shield, armament);

        if (robot.getCost() > player.getStat().getExperience()) {
            em.close();
            throw new IllegalArgumentException("You do not have enough coins!");
        }
        if (robot.getCost() == 0) {
            int i = 0;
            for (RobotConfiguration r : player.getRobots()) {
                if (r.getCost() == 0) {
                    i += 1;
                }
            }
            if (i >= 2) {
                throw new UnsupportedOperationException(
                        "A player can't have more then 2 standard robots!");
            }
        }
        player.getRobots().add(robot);
        armament.getRobots().add(robot);
        chassis.getRobots().add(robot);
        shield.getRobots().add(robot);
        em.persist(robot);
        em.getTransaction().commit();
        em.close();
    }

    /**
     * Delete a existing robot. The player must be the owner of this robot.
     * @param playerId id from the owner of the robort
     * @param robotId id from robot
     * @throws java.lang.IllegalArgumentException thrown if the user is unknown,
     * the robot is unknown or the user is not the owner.
     */
    public void deleteRobot(final String playerId, final int robotId)
            throws IllegalArgumentException {
        EntityManager em = emf.createEntityManager();
        em.getTransaction().begin();
        Player player = em.find(Player.class, playerId);
        if (player == null) {
            em.close();
            throw new IllegalArgumentException("user not found!");
        }
        RobotConfiguration robot = em.find(RobotConfiguration.class, robotId);
        if (robot == null) {
            em.close();
            throw new IllegalArgumentException("robot not found!");
        }
        if (!robot.getPlayer().equals(player)) {
            em.close();
            throw new IllegalArgumentException(
                    "user is not owner of this robot!");
        }
        if (robot.getCost() == 0) {
            throw new UnsupportedOperationException(
                    "Can't delete standard robot");
        }
        player.getRobots().remove(robot);
        em.getTransaction().commit();
        em.close();
    }

    /**
     *
     * @param robotID id for the wanted robot
     * @return if the robot exists, it will return the wanted robot, otherwise
     * null
     */
    public RobotConfiguration getRobot(final int robotID) {
        EntityManager em = emf.createEntityManager();
        RobotConfiguration robot = em.find(RobotConfiguration.class, robotID);
        em.close();
        return robot;
    }

    /**
     * Called from ranking-system at the initphase to load all players
     * @return map of players and there experience/ coins
     */
    public Map<String, Long> getPlayerCoins() {
        Map<String, Long> coinList = new HashMap<String, Long>();
        EntityManager em = emf.createEntityManager();

        List<Statistic> resultList = em.createQuery(
                "select s from Statistic s " +
                "ORDER BY s.experience DESC").getResultList();

        for (Statistic statistic : resultList) {
            coinList.put(statistic.getPlayername(), statistic.getExperience());
        }
        em.close();

        return coinList;
    }

    private List<Armament> readArmamentList() {
        List<Armament> list = new ArrayList<Armament>();
        File file = new File(Main.getConfDir().toString() +
                System.getProperty("file.separator") +
                ARMAMENT_FILE);
        List<List<String>> csvList = readCSVFile(file);
        List<String> entry;
        for (int i = 0; i < csvList.size(); i++) {
            try {
                entry = csvList.get(i);
                String name = entry.get(0);
                int power = Integer.valueOf(entry.get(1));
                int range = Integer.valueOf(entry.get(2));
                int weight = Integer.valueOf(entry.get(3));
                int cost = Integer.valueOf(entry.get(4));
                int levelRank = Integer.valueOf(entry.get(5));
                list.add(new Armament(name, power, range, weight, cost,
                        levelRank));
            } catch (NumberFormatException e) {
                logger.error(String.format("Irregular format of file %s " +
                        "in line %d", file, ++i));
                System.exit(1);

            } catch (IndexOutOfBoundsException e) {
                logger.error(String.format("Irregular format of file %s " +
                        "in line %d", file, ++i));
                System.exit(1);
            }
    }
        return list;
    }

    private List<Shield> readShieldList() {
        List<Shield> list = new ArrayList<Shield>();
        File file = new File(Main.getConfDir().toString() +
                System.getProperty("file.separator") +
                SHIELD_FILE);
        List<List<String>> csvList = readCSVFile(file);
        List<String> entry;
        for (int i = 0; i < csvList.size(); i++) {
            try {
                entry = csvList.get(i);
                String name = entry.get(0);
                int speed = Integer.valueOf(entry.get(1));
                int weight = Integer.valueOf(entry.get(2));
                int cost = Integer.valueOf(entry.get(3));
                int levelRank = Integer.valueOf(entry.get(4));
                list.add(new Shield(name, speed, weight, cost, levelRank));
            } catch (NumberFormatException e) {
                logger.error(String.format("Irregular format of file %s " +
                        "in line %d", file, ++i));
                System.exit(1);
            } catch (IndexOutOfBoundsException e) {
                logger.error(String.format("Irregular format of file %s " +
                        "in line %d", file,  ++i));
                System.exit(1);
            }
        }
        return list;
    }

    private List<Chassis> readChassisList() {
        List<Chassis> list = new ArrayList<Chassis>();
        File file = new File(Main.getConfDir().toString() +
                System.getProperty("file.separator") +
                CHASSIS_FILE);
        List<List<String>> csvList = readCSVFile(file);
        List<String> entry;
        for (int i = 0; i < csvList.size(); i++) {
            try {
                entry = csvList.get(i);
                String name = entry.get(0);
                int speed = Integer.valueOf(entry.get(1));
                int weight = Integer.valueOf(entry.get(2));
                int cost = Integer.valueOf(entry.get(3));
                int levelRank = Integer.valueOf(entry.get(4));
                list.add(new Chassis(name, speed, weight, cost, levelRank));
            } catch (NumberFormatException e) {
                logger.error(String.format("Irregular format of file %s " +
                        "in line %d", file,  ++i));
                System.exit(1);
            } catch (IndexOutOfBoundsException e) {
                logger.error(String.format("Irregular format of file %s " +
                        "in line %d", file,  ++i));
                System.exit(1);
            }
        }
        return list;
    }

    private List<List<String>> readCSVFile(File file) {
        List<List<String>> list = new ArrayList<List<String>>();
        try {
            BufferedReader reader = new BufferedReader(new FileReader(file));
            String line;
            List<String> strings;
            while ((line = reader.readLine()) != null) {
                strings = new ArrayList<String>();
                StringTokenizer tokenizer = new StringTokenizer(line, ",");
                while (tokenizer.hasMoreTokens()) {
                    strings.add(tokenizer.nextToken());
                }
                list.add(strings);
            }
            reader.close();

        } catch (FileNotFoundException ex) {
            logger.error(String.format("Didn't find %s",file));
            System.exit(1);
        } catch (IOException ex) {
            logger.error(ex.getMessage());
            System.exit(1);
        }
        return list;
    }
}
